
/*
 * Main.d: Application entry point.
 * Author: Guilherme R. Lampert
 * 2013-11-24
 */

import std.stdio;
import std.random;
import derelict.opengl3.gl;
import derelict.glfw3.glfw3;
import GLStuff;
import Inventory;
import GA;

// ======================================
// GA Inventory organizer application:
// ======================================

enum MAX_ITEM_SIZE  = 4;
enum CROSSOVER_RATE = 0.8;
enum MUTATION_RATE  = 0.05;
enum GENE_LENGTH    = (int.sizeof * 8); // Num bits per gene

class GAApplication {

	GeneticAlgorithm ga; // Genetic Algorithm helper
	Inventory originalInventory; // Inventory to sort

	// Initializes the GA demo:
	this(int inventoryW, int inventoryH, int numItems, int populationSize)
	{
		const uint numGenes     = cast(uint)(numItems * 2); // (2 genes per item: item.x, item.y)
		const uint chromoLength = cast(uint)(numGenes * GENE_LENGTH);

		// Randomly distribute items in the inventory:
		originalInventory = GenerateRandomInventory(inventoryW, inventoryH, numItems, MAX_ITEM_SIZE);

		// Set up population, one genome per inventory item:
		Genome[] population = new Genome[populationSize];
		for (int p = 0; p < population.length; ++p)
		{
			int[] data = new int[numGenes];
			int n = 0;

			// Give this genome every item in the inventory:
			for (int i = 0; i < originalInventory.items.length; ++i)
			{
				assert(originalInventory.items[i].IsValid());

				// Add some random noise to each genome:
				data[n + 0] = originalInventory.items[i].x + uniform!("[]")(-1, +1);
				data[n + 1] = originalInventory.items[i].y + uniform!("[]")(-1, +1);
				n += 2;
			}

			population[p] = new Genome(chromoLength, GENE_LENGTH);
			population[p].Encode(data);
		}

		// Create GA helper:
		ga = new GeneticAlgorithm(CROSSOVER_RATE, MUTATION_RATE, populationSize, chromoLength, GENE_LENGTH, population);
	}

	// Runs one iteration of the GA:
	void Run()
	{
		//debug writeln("Running generation: ", ga.generationNum);
		//debug writeln("Current population: ", ga.genomes.length);

		// Get fitness for the genomes:
		for (int i = 0; i < ga.genomes.length; ++i)
		{
			Inventory inventory = GetInventory(ga.genomes[i]);
			//inventory.DrawInventory();
			ga.genomes[i].fitness = inventory.EvaluateItemPlacement();
		}

		// Let the GA do its stuff:
		if (!ga.Run())
		{
			writeln("ERROR! Genetic Algorithm iteration failed!");
		}

		//debug writeln("Finished iteration...");
	}

	// Build an inventory from a genome, so we can extract its fitness from its item placement:
	Inventory GetInventory(ref Genome genome)
	{
		const uint numItems = cast(uint)originalInventory.items.length;
		const int maxX = originalInventory.width;
		const int maxY = originalInventory.height;

		int n = 0;
		int[] decoded = genome.Decode();
		assert(decoded.length == (numItems * 2));

		Item[] items = new Item[numItems];
		for (uint i = 0; i < numItems; ++i)
		{
			const int itemW = originalInventory.items[i].width;
			const int itemH = originalInventory.items[i].height;

			if ((decoded[n + 0] >= 0) && (decoded[n + 1] >= 0) &&
				((decoded[n + 0] + itemW) < maxX) && ((decoded[n + 1] + itemH) < maxY))
			{
				items[i] = new Item(decoded[n + 0], decoded[n + 1],
					itemW, itemH, originalInventory.items[i].symbol);
			}
			else
			{
				items[i] = new Item(); // Invalid item
			}

			n += 2;
		}

		return (new Inventory(originalInventory.width, originalInventory.height, items));
	}

	// Get the genome with the highest fitness score:
	ref Genome BestFit()
	{
		double bestFitness = 0.0;
		int iBest = 0;

		for (int p = 0; p < ga.genomes.length; ++p)
		{
			if (ga.genomes[p].fitness > bestFitness)
			{
				bestFitness = ga.genomes[p].fitness;
				iBest = p;
			}
		}

		return (ga.genomes[iBest]);
	}
}

// ======================================
// main():
// ======================================

void main(string[] args)
{
	// Brute force inventory organization or GA?
	bool bruteForce = true;

	// Quick arg parsing:
	foreach (string arg; args)
	{
		// See if the app is starting in console mode or GL mode:
		if (arg == "--no-gl")
		{
			noGL = true;
		}

		// Use Genetic Algorithms to organize the inventory:
		if (arg == "--use-ga")
		{
			bruteForce = false;
		}
	}

	if (noGL)
	{
		writeln("==== Starting in console text mode! ====");
	}
	else
	{
		writeln("==== Starting in GL mode! ====");
		if (!GLStartup("------ Initial inventory  --------------------  Sorted inventory ------"))
		{
			return;
		}
	}

	Inventory initialInventory = null;
	Inventory bestInventory = null;
	double bestRank = 0;

	if (bruteForce)
	{
		writeln("Sorting inventory using brute force method...");

		initialInventory = GenerateRandomInventory(10, 20, 45, MAX_ITEM_SIZE);

		const int w = initialInventory.width;
		const int h = initialInventory.height;
		const int cnt = cast(int)initialInventory.items.length;

		Item[] items = initialInventory.items;
		bestInventory = new Inventory(w, h, cnt);

		bestInventory.PlaceItemsLargerFirst(items);
		bestRank = bestInventory.EvaluateItemPlacement();
	}
	else
	{
		writeln("Sorting inventory using Genetic Algorithm method...");

		enum MAX_GENERATIONS = 1;
		enum POPULATION = 150;

		GAApplication app = new GAApplication(10, 20, 45, POPULATION);
		initialInventory = app.originalInventory;

		for (ulong i = 0; i < MAX_GENERATIONS; ++i)
		{
			app.Run();
		}

		Genome best = app.BestFit();
		bestInventory = app.GetInventory(best);
		bestRank = bestInventory.EvaluateItemPlacement();
	}

	assert(initialInventory !is null);
	assert(bestInventory !is null);
	writeln("***** Sorted inventory rank: ", bestRank);

	if (noGL)
	{
		writeln("Initial inventory:");
		initialInventory.DrawInventory(0);

		writeln("Sorted inventory:");
		bestInventory.DrawInventory(1);
	}
	else
	{
		// Loop until the user closes the window:
		while (!glfwWindowShouldClose(glWindow))
		{
			glClear(GL_COLOR_BUFFER_BIT);

			// To the right, the original inventory:
			initialInventory.DrawInventory(0);

			// And the sorted one to the left:
			bestInventory.DrawInventory(1);

			glfwSwapBuffers(glWindow);
			glfwPollEvents();
		}

		GLShutdown();
	}
}
